Skip to content
核心概念

核心概念

理解 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.c
make 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 -nmake -B 是黄金组合:前者看命令,后者看命令是否还能成功。