核心概念
理解 Make 的核心是理解规则(Rule)和依赖图(Dependency Graph)。本章讲清楚 Make 如何决定「要不要跑、跑什么、按什么顺序跑」。
规则的基本结构
一条规则由三部分组成:
目标(target): 依赖(prerequisites)
命令(recipe)
命令...
# 示例
app: main.go util.go
go build -o app main.go util.go
| 部分 | 含义 | 说明 |
|---|---|---|
| target | 目标 | 通常是要生成的文件名,或动作名(配合 .PHONY) |
| prerequisites | 依赖 | 生成目标前需要先就绪的文件或其它目标 |
| recipe | 命令 | 实际执行的 shell 命令,每行前必须是 Tab |
🚨 Tab 而非空格:recipe 的每一行必须以 Tab 开头。用 4 个空格会报
*** missing separator. Stop.。这是 Make 的历史包袱,也是最高频的坑。
Make 如何决定是否执行(时间戳判断)
Make 的执行逻辑是基于文件时间戳的:
目标存在 且 目标时间戳 ≥ 所有依赖的时间戳 → 跳过(已是最新)
目标不存在 或 某个依赖比目标新 → 执行命令app: main.go util.go
go build -o app main.go util.go
# 场景演示:
# 1. app 不存在 → 执行 go build
# 2. app 存在,main.go 更新 → 执行 go build
# 3. app 存在且都比依赖新 → 跳过,输出 "make: 'app' is up to date."
🔬 深入原理:Make 本质上是一个「增量构建系统」。它不关心命令内容,只比较文件时间戳。这也解释了为什么纯动作类目标(无产物文件)必须声明
.PHONY——否则同名文件存在时会被误判为「已最新」而跳过。
依赖图与执行顺序
多个规则之间会形成依赖关系,Make 会深度优先地先构建依赖:
app: main.o util.o
gcc -o app main.o util.o
main.o: main.c
gcc -c main.c
util.o: util.c
gcc -c util.c
app
/ \
main.o util.o
| |
main.c util.cmake app
# 执行顺序:
# 1. 先检查并构建 main.o、util.o(依赖先于目标)
# 2. 再构建 app💡 依赖图中的节点如果没有对应规则,Make 会去检查是否存在同名文件;都不存在则报错
No rule to make target 'xxx'。
默认目标
不传任何参数执行 make 时,会构建 Makefile 中的第一个目标(忽略以 . 开头的特殊目标):
build:
go build ./...
test:
go test ./...
make # 等价于 make build(第一个目标)可以用 .DEFAULT_GOAL 显式指定默认目标(推荐,可读性更好):
.DEFAULT_GOAL := help
help:
@echo "用法: make <target>"
build:
go build ./...
💡 项目惯例:把
help设为默认目标,让「裸敲make」就展示用法,而不是意外触发 build。
.PHONY(伪目标)
.PHONY 声明的目标不对应真实文件,无论同名文件是否存在、是否最新,都会执行:
.PHONY: build test clean
build:
go build ./...
test:
go test ./...
clean:
rm -rf bin/
为什么需要它:
| 场景 | 不声明 .PHONY 的后果 |
|---|---|
目录里恰好有个 build 文件 |
make build 判断「已最新」而跳过 |
| 目标永远不生成文件 | 每次执行都要靠「目标不存在」才触发,逻辑脆弱 |
| 语义表达 | 读者无法区分「文件产物」和「动作」 |
💡 经验法则:凡是「动作类」目标(build/test/run/clean/lint/deploy…)一律声明
.PHONY;凡是「文件产物」目标(app、%.o、dist/xxx.tar.gz)不声明。
自动变量(速览)
自动变量在 recipe 中引用「当前规则」的信息,让规则可以复用:
app: main.go util.go
go build -o $@ $^
# $@ = 目标 = app
# $^ = 所有依赖 = main.go util.go
| 变量 | 含义 | 示例值 |
|---|---|---|
$@ |
目标名 | app |
$< |
第一个依赖 | main.go |
$^ |
所有依赖(去重) | main.go util.go |
$? |
比目标新的依赖 | util.go(若只有它更新) |
$* |
模式规则中的匹配部分(stem) | 见模式规则章节 |
详细展开见
02-变量与自动变量.md。
命令前缀修饰符
recipe 的每行命令可加前缀控制行为:
build:
@echo "这条命令本身不显示,只显示结果"
-rm -f 可能不存在的文件 # - 表示忽略失败,继续执行
+make sub # + 表示即使 -n/-t 模式也执行(递归 make 用)
| 前缀 | 作用 |
|---|---|
@ |
不回显命令本身 |
- |
命令失败也继续(不中断) |
+ |
即使 dry-run/exec 模式也执行 |
调试技巧
make -n # 只看将执行什么,不执行(最常用)
make -B # 强制重跑,验证命令是否仍能成功
make --debug=v # 打印变量展开过程
make --debug=b # 打印规则匹配过程
# 打印某个变量的值(在 Makefile 中临时加):
debug:
@echo "GO := $(GO)"💡 排查「为什么这条命令没跑」时,
make -n加make -B是黄金组合:前者看命令,后者看命令是否还能成功。