欢迎光临
我们一直在努力

【Linux 系统篇(九)】基础开发工具(四) —— 自动化构建 make/Makefile

在这里插入图片描述

大家好,欢迎来到 huangjin007_ 的博客

个人主页:huangjin007_
🔥
文章收录专栏:Linux 内功修炼手册(系统篇)

总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇


Linux 系统篇(九) —— 自动化构建 make/Makefile 详解

文章目录

  • Linux 系统篇(九) —— 自动化构建 make/Makefile 详解
    • 一、为什么要学 Make 和 Makefile?
      • 1.1 手动编译
      • 1.2 Make 的核心思想:增量编译
    • 二、生活化的例子
      • 2.1 月底要钱的例子
      • 2.2 翻译成 Makefile 的思维
    • 三、第一个 Makefile
      • 3.1 准备源代码
      • 3.2 手写一个最简单的 Makefile
      • 3.3 分析这个 Makefile 的结构
      • 3.4 运行一下
      • 3.5 清理:`clean` 与 `.PHONY`
    • 四、文件的修改时间
      • 4.1 stat 命令
      • 4.2 Make 如何利用 Modify 时间?
    • 五、Make 的工作方式与依赖链
      • 5.1 一个展示编译全过程的 Makefile
      • 5.2 Make 的工作步骤
    • 六、扩展语法
      • 6.1 变量定义
      • 6.2 自动获取源文件
      • 6.3 模式替换
      • 6.4 自动化变量
      • 6.5 命令回显控制
      • 6.6 综合示例
    • 结语:

一、为什么要学 Make 和 Makefile?

1.1 手动编译

  如果你在做一个 C 语言项目,刚开始只有一个源文件 main.c,编译很简单:

gcc main.c -o app

  过两天项目扩展了,增加了 util.c、net.c、db.c……你不得不这么编译:

gcc -c main.c -o main.o
gcc -c util.c -o util.o
gcc -c net.c -o net.o
gcc -c db.c -o db.o
gcc main.o util.o net.o db.o -o app

  如果某个头文件改了,或者你在 util.c 里修了一个小 bug,难道要把所有文件都重新编译一遍?这太浪费时间了。

  那能不能只编译那些真正发生变化的部分?

1.2 Make 的核心思想:增量编译

  make 会根据文件的 修改时间 自动判断哪些文件需要重新编译。它的逻辑很简单:

  • 每个目标文件(比如 .o)都依赖某些源文件(比如 .c )。
  • 如果依赖文件的“最后修改时间”比目标文件新,说明源文件改过了,目标文件需要重新生成。
  • 否则,就不需要动它。

  这样一来,每次你只改了一个文件,make 就只重新编译受影响的部分,效率就能得到提高。


二、生活化的例子

2.1 月底要钱的例子

你是一个大学生,每个月月底都要向爸妈要生活费。这个行为可以描述为:

  • 目标:拿到生活费。
  • 依赖:爸妈的工资到账。
  • 规则:如果工资已到账,就打电话要钱。

你发现规律了吗?   如果工资一直不到账(依赖不满足),要钱的动作就不会发生。如果上个月的生活费还没花完(目标已存在且比依赖新),你也可能不急着要。但如果你声明“不管怎样,每个月必须执行一次要钱操作”,那就类似于 Makefile 中的 伪目标 .PHONY。

2.2 翻译成 Makefile 的思维

生活费: 爸妈工资到账
打电话要钱

  • 冒号前面是目标,后面是依赖。
  • 下一行缩进(必须用 Tab 键!)是达成目标需要执行的命令。

  make 会检查“爸妈工资到账”这个依赖的状态,然后决定是否执行“打电话要钱”。


三、第一个 Makefile

3.1 准备源代码

创建一个 myproc.c:

#include <stdio.h>
int main()
{
printf("hello Makefile!\\n");
return 0;
}

3.2 手写一个最简单的 Makefile

  新建一个名为 Makefile 或 makefile 的文件,内容如下:

myproc: myproc.c
gcc -o myproc myproc.c

.PHONY: clean
clean:
rm -f myproc

3.3 分析这个 Makefile 的结构

每一个构建规则都由三部分组成:

组成部分说明示例
目标 你想生成的文件,或者是一个动作的名字 myproc、clean
依赖 生成目标需要的原材料 myproc.c
命令 如何用依赖生成目标,必须以 Tab 开头 gcc -o myproc myproc.c

  整个逻辑就是:如果目标文件不存在,或者任何一个依赖文件比目标文件新,就执行下面的命令。

3.4 运行一下

在终端输入:

make

输出:

gcc -o myproc myproc.c

可执行文件 myproc 生成了。再执行一次 make,会提示:

make: 'myproc' is up to date.

  因为 myproc 已经存在,而且它的修改时间比 myproc.c 新,不需要重新编译。

3.5 清理:clean 与 .PHONY

  项目开发过程中会产生很多中间文件(.o、.i、.s 等)和最终可执行文件,我们需要一个清理动作。上面的 Makefile 里定义了 clean 目标:

clean:
rm -f myproc

  执行 make clean,就会删除 myproc。

  但有个陷阱:如果当前目录下恰好有一个文件也叫 clean,那么 make clean 就傻掉了。 它会认为目标 clean 已经存在且依赖为空,无需更新,于是什么都不做。

用 .PHONY 修饰后:

.PHONY: clean

  这样 clean 就成为一个“伪目标”,无论同名的文件是否存在,它后面的命令都会无条件执行。这也就是为什么“总是被执行”的意思。


四、文件的修改时间

4.1 stat 命令

  在 Linux 中,文件由“内容”和“属性”两部分组成。使用 stat 命令可以查看文件的时间戳:

stat myproc.c

输出示例: 在这里插入图片描述

这里有三个时间:

  • Access(访问时间):文件最近一次被读取的时间。
  • Modify(内容修改时间):文件内容最后一次被修改的时间。这是 make 最关心的,也是判断是否重新编译的根本依据。
  • Change(属性修改时间):文件元数据(权限、拥有者等)最后一次被改变的时间。

4.2 Make 如何利用 Modify 时间?

  假设现在有 myproc 和 myproc.c 两个文件:

  • 如果 myproc 不存在,那肯定要编译。
  • 如果 myproc 存在,但 myproc.c 的 Modify 时间比 myproc 的 Modify 时间更新,说明源代码改过了,需要重新编译。
  • 如果 myproc.c 的时间更旧,则无需重编。

  记住结论:.PHONY 会让 make 完全忽略文件修改时间的对比,强制执行命令。


五、Make 的工作方式与依赖链

5.1 一个展示编译全过程的 Makefile

  回顾 C 语言编译的四个步骤:预处理 -> 编译 -> 汇编 -> 链接,对应生成 .i、.s、.o 和最终可执行文件。我们可以写一个能展示完整流程的 Makefile:

在这里插入图片描述

执行 make,输出:

gcc -E myproc.c -o myproc.i
gcc -S myproc.i -o myproc.s
gcc -c myproc.s -o myproc.o
gcc myproc.o -o myproc

  神奇的是,我们只写下了目标和依赖,make 就自动按顺序一步步生成了所有文件。

5.2 Make 的工作步骤

  在默认方式下(只输入 make 命令),make 的运作流程如下:

  • 寻找 Makefile:在当前目录下寻找名为 Makefile 或 makefile 的文件。
  • 定位最终目标:找到文件中的第一个目标,把它作为“终极任务”,也就是我们要生成的最终文件(上例中是 myproc)。
  • 检查终极目标的状态:如果 myproc 不存在,或者它的依赖 myproc.o 比它新,就执行链接命令生成 myproc。
  • 递归处理依赖:在检查 myproc.o 时,发现它不存在,于是 make 继续在当前 Makefile 中寻找以 myproc.o 为目标的规则。找到后,又会检查它的依赖 myproc.s……就像栈一样,一层层向下深入。
  • 自底向上执行:直到某个依赖(比如 myproc.c)已经存在且不需要更新,make 就从最底层开始执行命令,先预处理得到 .i,再编译得到 .s,再汇编得到 .o,最后链接成可执行文件。
  • 错误处理:如果某个依赖文件最终找不到,make 会报错并退出。但对于命令本身的编译错误,make 只会如实报告,不会自行处理。
  •   这个过程可以总结为:自上而下解析依赖关系,自下而上执行构建命令。


    六、扩展语法

    6.1 变量定义

      Makefile 中可以用 变量名=值 来定义变量,引用时用 $(变量名) 或 ${变量名}。例如:

    在这里插入图片描述

      这样修改编译器或输出文件名时,只改变量定义即可。

    6.2 自动获取源文件

      wildcard 函数可以匹配文件名:

    SRC=$(wildcard *.c)

      这会把当前目录下所有 .c 文件赋值给 SRC。

    6.3 模式替换

      有了源文件列表,我们还需要得到对应的目标文件列表(.o 文件)。可以用模式替换:

    OBJ=$(SRC:.c=.o)

      这里 $(SRC:.c=.o) 的意思是把 SRC 中所有以 .c 结尾的字符串,替换成以 .o 结尾。假设 SRC 是 main.c util.c,那么 OBJ 就是 main.o util.o。

    6.4 自动化变量

      Makefile 提供了一些特殊的变量,在规则中非常实用:

    • $@ :代表当前规则中的目标文件名。
    • $< :代表当前规则的第一个依赖文件名。 当只有单个依赖时,$< 就是指那个源文件 myproc.c。
    • $^ :代表当前规则的所有依赖文件列表(以空格分隔)。 如果依赖有多个,$^ 会展开为 file1.c file2.c …

      举个简单的例子:

    $(BIN): $(OBJ)
    gcc $^ -o $@

      假如 BIN=app,OBJ=main.o util.o,那么实际执行的命令就是:

    gcc main.o util.o -o app

      再比如模式规则:

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

      %.o: %.c 表示:对于任何要生成的 .o 文件,它的依赖是对应的同名 .c 文件。$< 在这里代表第一个依赖,也就是那个 .c文件;$@ 就是目标 .o 文件。

      对于 main.o,$< 是 main.c,$@ 是 main.o。这样一条规则就能处理所有 .c 到 .o 的编译。

    6.5 命令回显控制

      默认情况下,make 会把执行的命令打印到屏幕上。如果不想显示命令本身(例如为了输出更清晰),可以在命令前加 @:

    @echo "正在编译…"

      这样屏幕只会显示 正在编译…,而不会打印 echo 命令本身。

    6.6 综合示例

      现在我们把以上特性整合起来,得到一个相对完整的工程 Makefile:

    在这里插入图片描述

    解读:

    • 变量区:把所有可配置项放在最前面,方便修改。
    • 最终目标 $(BIN): $(OBJ):可执行文件依赖所有 .o 文件,链接命令使用自动化变量。
    • 模式规则 %.o: %.c:这是 make 的隐式规则骨架,告诉 make 任何一个 .o 文件都可以通过同名的 .c 文件编译得到。$< 代表被编译的那个 .c 文件。
    • clean:清理所有生成文件,伪目标,确保一定会执行。

    结语:

      今天的内容到这里就结束了,希望你能有所收获~

    干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux 系统篇(九)】基础开发工具(四) —— 自动化构建 make/Makefile
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址