Make 用法速查:从入门规则到工程级 Makefile 模板
make 是 GNU 构建工具,核心思想只有一句话:根据「目标 ← 依赖」的时间戳关系,只重跑必要的命令。理解了这一点,Makefile 里绝大多数看似古怪的写法都能想通。
这篇是实用速查,覆盖规则语法、变量、自动变量、函数、常见工程模板,以及最容易踩的坑。
一、最基本的规则
target: prerequisites
command # 注意:必须是 TAB 缩进,不是空格
一个 C 项目的最小例子:
app: main.o util.o
gcc -o app main.o util.o
main.o: main.c defs.h
gcc -c main.c
util.o: util.c defs.h
gcc -c util.c
执行 make 会构建第一个目标;make app 构建指定目标。make 会比对目标与依赖的修改时间,依赖更新才重新执行命令。
二、命令行参数速查
| 命令 | 作用 |
|---|---|
make |
构建第一个目标 |
make target |
构建指定目标 |
make -f other.mk |
指定 makefile 文件 |
make -j8 |
8 个任务并行 |
make -n / --dry-run |
只打印命令,不执行 |
make -B |
无条件全部重建 |
make -C dir |
先切到 dir 再构建 |
make -k |
出错继续构建其他目标 |
make -p |
打印所有规则和变量(调试利器) |
make VAR=value |
覆盖 makefile 中的变量 |
三、变量
CC := gcc # := 立即展开
CFLAGS = -O2 -Wall # = 延迟展开(递归)
CFLAGS += -g # 追加
OBJS := $(SRCS:.c=.o) # 后缀替换
SRCS := $(wildcard src/*.c)
四种赋值方式的区别值得记牢:
A = $(B) # 递归展开,用到时才求值(B 可以后定义)
B := $(A) # 简单展开,立刻求值
C ?= x # 仅当未定义时赋值
D != date # 执行 shell 命令取值(GNU make 4.0+)
引用写法是 $(CC) 或 ${CC}。命令行传入的 make CC=clang 优先级最高,除非 makefile 里用了 override。
四、自动变量
规则内部可用的快捷变量:
| 变量 | 含义 |
|---|---|
$@ |
目标名 |
$< |
第一个依赖 |
$^ |
所有依赖(去重) |
$+ |
所有依赖(不去重) |
$* |
模式匹配中 % 命中的部分 |
$(@D) / $(@F) |
目标所在目录 / 目标文件名 |
配合模式规则使用:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
五、.PHONY 与伪目标
.PHONY: all clean install test
all: app
clean:
rm -f *.o app
test:
./app --test
clean 这类不产生同名文件的目标必须声明 .PHONY,否则目录里一旦存在同名文件,规则会被静默跳过——这是新手最常见的「为什么没执行」问题。
六、常用函数
$(wildcard *.c) # 匹配文件
$(patsubst %.c,%.o,$(SRCS)) # 模式替换
$(SRCS:.c=.o) # 后缀替换(简写)
$(shell ls -d */) # 执行命令
$(foreach f,$(SRCS),$(f).bak)
$(filter %.c,$(FILES))
$(dir src/a.c) # src/
$(notdir src/a.c) # a.c
$(basename src/a.c) # src/a
$(addprefix obj/,$(OBJS))
七、工程级模板
下面这个模板可以直接拿去改:
CC := gcc
CFLAGS := -O2 -Wall -MMD -MP
SRCS := $(wildcard src/*.c)
OBJS := $(SRCS:src/%.c=build/%.o)
TARGET := build/app
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $^ -o $@
build/%.o: src/%.c | build
$(CC) $(CFLAGS) -c $< -o $@
build:
mkdir -p build
clean:
rm -rf build
-include $(OBJS:.o=.d)
.PHONY: all clean
三个关键点:
-MMD -MP让编译器额外生成.d头文件依赖文件,改动头文件也能正确触发重编译;| build是 order-only 依赖:build 目录不存在才创建,目录自身时间戳变化不算过期;-include前面的-表示文件不存在时不报错(首次构建时.d还不存在)。
八、多目标与静态模式规则
# 一条规则生成多个文件(GNU make 4.3+ 的 & 分组目标)
a.txt b.txt &: gen.py
python gen.py
# 静态模式规则:只对列表中每个目标套用模式
$(OBJS): build/%.o: src/%.c
$(CC) -c $< -o $@
九、条件判断与递归调用
ifeq ($(DEBUG),1)
CFLAGS += -g -O0
else
CFLAGS += -O2
endif
subsystem:
$(MAKE) -C lib
递归调用子 make 一定要用 $(MAKE) 而不是裸 make,这样 -j、-n、-k 等参数才能正确传递下去。
十、常见坑
- 命令行前用了空格而不是 TAB,报
missing separator。 - 每条命令行都在独立 shell 中执行,
cd不会跨行生效;要写成cd dir && cmd,或使用.ONESHELL:。 - shell 变量要写成
$$,例如$$HOME。 - 自动变量是单字符的,
$X只识别一个字符;自定义变量要用$(NAME)。 - 忘记
.PHONY,同名文件存在时目标被静默跳过。 - 不知道为什么不重编译时,用
make -n看计划、make -d看依赖推导过程。
小结
Makefile 的复杂度几乎全部来自两件事:时间戳依赖的传递和变量展开时机。把这两点想清楚,再掌握自动变量与几个高频函数,就足以写出稳定、可维护的构建脚本。真正需要诊断时,make -n 和 make -d 比反复猜快得多。