Makefile 指令

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

364.3.3 Makefile 指令

本节只在思维导图、Keil µVision 工程设置和网页文章中讲解 Makefile 概念,没有打开或创建课程配套的 main.c、hello_pr.c、头文件或 Makefile,也没有在终端中运行 make。因此,本篇只还原画面中的命令和规则伪代码,不提前收录下一节才开始编写的业务源码与 Makefile。

一、Makefile指令

先区分三个容易混淆的名称:

名称准确含义
make读取构建描述、判断哪些目标需要更新并执行相应配方的程序;本课程使用的是 GNU make
makefile对这类构建描述文件的通称
MakefileGNU make 默认查找的常见文件名之一,也是课程后续采用的文件名

把 Makefile 简单称为“脚本”有助于入门,但不够准确。它更接近一种声明构建关系的领域专用语言:规则描述“目标依赖什么、需要更新时怎样生成”,变量和函数用于组织文本,规则中的配方通常再交给 Shell 执行。Makefile 本身不是 GCC,也不限于调用 GCC;只要命令行工具能完成某项工作,原则上都可以写进配方。

视频先把单文件命令:

gcc main.c -o main

扩展成多文件命令:

gcc main.c 1.c 2.c 3.c 4.c -o main

后一条命令在这些源文件都存在且能共同链接时是可用的,但每次手工输入既容易漏文件或选项,也难以只重建发生变化的部分。源文件增多、目录变深、编译选项和库依赖变复杂后,应该把这些信息纳入可版本管理、可复现的构建系统。

头文件还需要单独说明:通常不把 .h 文件当作独立翻译单元直接放进 GCC 命令,而是由 .c 文件中的 #include 纳入编译。Makefile 仍应记录相关头文件依赖,否则头文件改变后,make 可能不知道对应目标文件已经过期。头文件不在默认搜索路径时,还要用 -I 等编译选项指定搜索目录。

视频实际展示的是 Keil µVision,不是原笔记识别出的 Qt。µVision 的图形界面允许配置源文件、宏、包含目录、优化级别和警告选项,然后根据工程配置调用相应工具链。图形化 IDE 隐藏了许多命令行细节,但不能据此断言它一定“自动生成 Makefile”;不同 IDE 可能读取自己的工程文件,也可能调用 Make、Ninja 或其他构建后端。

“Linux 下没有图形化配置,只能逐行写 Makefile”同样不准确。Linux 上也有多种 IDE 和构建系统,GNU make 也能运行在 GNU/Linux 之外。课程选择 Makefile,是为了直接理解构建依赖和工具链调用,而不是因为 Linux 只能使用它。

1.1 Makefile入⻔

1.1.1 什么是Makefile

Makefile 告诉 make:

  1. 最终希望更新哪个目标;
  2. 这个目标依赖哪些文件或其他目标;
  3. 目标过期时,应执行什么配方来更新它。

执行不带目标名的 make 时,GNU make 通常以第一条合适规则的第一个目标作为默认目标,再递归检查它的依赖。也可以显式执行 make target,要求更新指定目标。

GNU make 默认会按规定顺序查找 GNUmakefile、makefile、Makefile。实际项目最常使用便于识别的 Makefile;如果采用其他文件名,可通过 make -f 文件名 指定。因此,“文件名只能是首字母大写的 Makefile,否则完全无法识别”也不是严格规则,只是课程采用的推荐命名约定。

对普通文件目标,make 主要依据文件是否存在及最后修改时间判断是否需要更新:

  • 目标不存在:需要执行配方;
  • 任一普通依赖比目标更新:需要执行配方;
  • 依赖自身需要更新:先更新依赖,再重新判断目标;
  • 目标不比任何依赖旧:通常认为它已是最新,不执行配方。

这正是 Makefile 能实现增量构建的基础。不过,make 不会自动理解任意源代码的语义;如果依赖关系漏写,即使某个文件内容已经影响目标,它也可能错误地认为无需重建。

1.1.2 为什么要使用Makefile

与反复手敲一条很长的 GCC 命令相比,Makefile 的主要价值包括:

  • 可复现:源文件列表、宏、头文件路径、优化、警告和链接选项可以随项目一起保存;
  • 增量构建:依赖正确时,只更新过期目标,而不是无条件重编整个工程;
  • 统一入口:开发者和持续集成环境可以通过同一组目标执行构建、测试、清理等任务;
  • 模块化:可以包含公共规则或依赖文件,也可以组织对子目录构建的调用;
  • 并行执行:依赖图正确时,GNU make -j 可以并行更新彼此独立的目标。

视频说企业项目不会允许直接使用 GCC 命令、Makefile 通常只由“配置管理员”编写,这些都不是通用规则。小型验证、故障排查和自动化脚本仍可能直接调用编译器;谁维护构建系统则取决于团队分工。开发者至少应能读懂目标、依赖、变量和配方,并能判断修改是否破坏增量构建和交叉编译配置。

学习所需时间也因基础和项目复杂度而异,不能保证“五六小时完全学会”。入门规则可以很快掌握,但大型工程还会涉及自动依赖生成、模式规则、递归或非递归组织、并行安全、静态库、交叉编译和配置管理等问题。

1.1.3 Makefile规则

GNU make 的基本规则形式为:

targets: prerequisites
	recipe

也可以把第一条配方写在分号后:

targets: prerequisites ; recipe

这里代码块第二行开头是一个真正的 Tab 字符。GNU make 默认以 Tab 识别配方行;用空格替代通常会触发类似 missing separator 的错误。虽然 GNU make 可通过特殊变量 .RECIPEPREFIX 更改前缀,但初学阶段应坚持使用 Tab。

组成部分准确含义
目标(target)要生成或更新的文件,也可以是 clean 这类代表动作的名字
依赖(prerequisite)判断目标是否过期以及建立执行顺序所依据的文件或其他目标;可以为零个、一个或多个
配方(recipe)目标需要更新时执行的一行或多行命令,默认由 Shell 解释

原笔记中的 “pre-request” 是识别错误,正确术语是 prerequisite。GNU make 的官方术语是 recipe,旧资料中也常写 command;为了避免与 make 自身的语法混淆,本篇统一称为“配方”。

视频在网页代码框上手写了下面这段内容:

main.o: main.c 1.c 2.c 3.c
    gcc 把依赖编译成 main.o

这只是用中文说明 target、prerequisites、recipe 位置的伪代码,不是可以直接保存运行的 Makefile。第二行不是合法 Shell 命令,而且把多个独立 .c 文件直接说成一起“编译成一个 main.o”也不严谨:通常每个 .c 分别编译为自己的 .o,最后再把多个 .o 链接成可执行文件。

一个只用于说明语法、且不引入课程业务代码的单文件规则可以写成:

main.o: main.c
	gcc -c main.c -o main.o

它表达的是:如果 main.o 不存在,或者 main.c 比 main.o 更新,就执行配方生成 main.o。make 不会像视频口述的那样把“依赖自动放到 GCC 命令的依赖位置、目标自动放到目标位置”;配方必须显式写出文件名,或在后续课程中使用 $@、

lt;、$^ 等自动变量引用规则上下文。

如果规则有多行配方,GNU make 默认逐行启动 Shell。需要让多个 Shell 操作共享同一工作目录或变量时,不能想当然地把它们拆成多行;可以在一条逻辑配方中正确使用 Shell 连接符,或使用 GNU make 的相关机制。反斜杠加换行可以续写较长的逻辑行,但它在 Makefile 普通语法和配方中的具体处理并不完全相同,不能简单理解为删除换行。

1.1.3.1 显式规则

显式规则与隐式规则

显式规则由编写者明确写出目标和依赖,并可写出配方。隐式规则则描述一类常见的文件转换方式。例如 GNU make 内置了从 .c 推导 .o 的常用规则,也允许项目自行定义模式规则。

隐式规则不是“自动找出项目中所有目标和真实依赖”。它只能根据目标文件名、已有文件、内置规则或项目定义的模式规则进行推导;源文件包含了哪些头文件,也不会因为使用隐式规则就天然全部记录下来。必要时仍要明确依赖,或让编译器生成依赖文件。

还要把两个符号分开:文件名通配符使用 *、?、[...],GNU make 的模式规则通常使用 % 表示可匹配的“词干”。例如 %.o: %.c 表示同名 .o 可以由 .c 推导;*.c 只是匹配文件名,并不是“调用隐式规则”的特殊符号。

变量

视频在画面中写了:

num = 1
num := 1

两者都把文本赋给 Make 变量,但展开时机不同:

  • = 定义递归展开变量,右侧文本通常在每次引用变量时再展开;
  • := 定义立即展开变量,右侧会在这条定义被读取时展开一次,再保存结果;
  • 引用变量通常写成 $(num) 或 ${num}。

Make 变量的值本质上是文本,不是“赋数字就自动变成整数、赋单词就自动变成字符串”的动态类型系统。某些函数或配方中的外部命令可以把文本解释成数字,但变量本身没有 C 语言那样的 int、char * 等类型。

包含文件与模块化构建

include common.mk

include 指令会暂停读取当前 Makefile,读取指定文件,然后继续读取原文件。它适合共享变量、规则或编译器生成的依赖片段,但这不等于“运行另一个 Makefile”。如果希望在子目录启动另一个 make 实例,通常会在配方中使用 $(MAKE) -C 子目录;包含片段与递归调用是两种不同的工程组织方式。

所以,视频中的 APP、M1、M2、M3 图只是在说明模块可以分别维护构建信息。真实工程既可以由顶层 Makefile 包含各模块的 .mk 片段,形成一张统一依赖图,也可以递归调用各子目录的 Makefile;最终必须由链接规则明确组合各模块生成的目标文件或库,而不是“调用一下 Makefile 就自然链接到 APP”。

条件判断与注释

GNU make 提供 ifeq、ifneq、ifdef、ifndef、else、endif 等条件指令。它们在 make 读取 Makefile 时决定哪些文本参与解析,不是 C 预处理器,也不是运行到配方时才判断的普通 Shell if。变量值 0 和 1 也没有天生的真假语义,必须通过明确的比较或非空判断表达条件。

在 Makefile 普通语法中,未被转义的 # 开始注释,直到该逻辑行结束;需要字面量 # 时可写成 \#。配方中的文本最终还会交给 Shell,因而配方内的注释与转义还要考虑 Shell 规则,不能只套用 C 的 // 或 /* ... */。

1.1.3.2 规则中的通配符

GNU make 的文件名通配符与 Bourne Shell 风格相近:

模式含义示例
*匹配零个或多个字符*.c 匹配当前目录中以 .c 结尾的文件
?匹配恰好一个字符aaa?.c 可匹配 aaa1.c,不能匹配 aaa10.c
[abc]匹配集合中的一个字符file[abc].c 可匹配 filea.c、fileb.c、filec.c
[0-9]匹配范围中的一个字符file[0-9].c 可匹配 file0.c 至 file9.c
[^abc]在 GNU/Linux 常见文件名匹配环境中表示匹配集合之外的一个字符;也常写作 [!abc]file[^abc].c 不匹配 filea.c、fileb.c、filec.c

因此,视频把 * 说成“任意一个或多个字符”少了“零个”这种可能;? 才是恰好一个字符。通配符只负责匹配文件名,*.c 本身不会自动编译任何文件。

通配符的展开位置也很重要:

  • 在规则的目标和依赖中,GNU make 会执行通配符展开;
  • 在配方中,通配符通常由 Shell 在命令执行时展开;
  • 在普通变量赋值中,SOURCES = *.c 保存的通常仍是字面文本 *.c;若要在赋值时得到现有文件列表,应使用 SOURCES := $(wildcard *.c);
  • *.c 只匹配当前这一层目录,不会递归搜索所有子目录。

方括号模式匹配的是一个位置上的一个字符,不是任意长度的字符串。不同系统的文件名匹配实现可能存在细节差异,需要跨平台时应在实际使用的 GNU make 与 Shell 环境中验证。

1.1.3.3 通配符示例

在 Makefile 的规则依赖中,下面的 *.c 会展开为当前目录内已存在的 C 源文件名:

target: *.c

但这条规则没有配方,并不等于“编译所有 .c 文件”。还需要显式规则、模式规则或适用的内置隐式规则,说明如何生成目标。

若要在 Shell 中递归查找当前目录及其子目录内的 .c 文件,较准确的写法是:

find . -type f -name '*.c'

这里 . 表示从当前目录开始,-type f 限定普通文件,-name '*.c' 按文件名匹配。给 *.c 加引号,是为了把模式交给 find,避免先被当前 Shell 展开。视频和原笔记中的 find .c、find *.c 都没有完整表达这个递归查找条件。

画面还把 ~ 列在通配符一节中。严格说它不是 *、?、[...] 这类通配符;当 ~ 位于文件名开头时,GNU make 可把 ~ 或 ~/... 展开为当前用户的主目录,把 ~user/... 展开为指定用户的主目录(取决于系统支持)。它不会执行 cd,也不表示“回到编译目录”。

最后要牢记三组容易混淆的符号:

符号所属语境作用
*、?、[...]文件名通配匹配现有文件名
%GNU make 模式规则表示目标与依赖文件名中对应的词干
$()、${}Make 变量或函数引用展开变量值或函数结果

参考资料