Make 用法速查:从入门规则到工程级 Makefile 模板

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  

三个关键点:

  1. -MMD -MP 让编译器额外生成 .d 头文件依赖文件,改动头文件也能正确触发重编译;
  2. | build 是 order-only 依赖:build 目录不存在才创建,目录自身时间戳变化不算过期;
  3. -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 等参数才能正确传递下去。

十、常见坑

  1. 命令行前用了空格而不是 TAB,报 missing separator。
  2. 每条命令行都在独立 shell 中执行,cd 不会跨行生效;要写成 cd dir && cmd,或使用 .ONESHELL:。
  3. shell 变量要写成 $$,例如 $$HOME。
  4. 自动变量是单字符的,$X 只识别一个字符;自定义变量要用 $(NAME)。
  5. 忘记 .PHONY,同名文件存在时目标被静默跳过。
  6. 不知道为什么不重编译时,用 make -n 看计划、make -d 看依赖推导过程。

小结

Makefile 的复杂度几乎全部来自两件事:时间戳依赖的传递和变量展开时机。把这两点想清楚,再掌握自动变量与几个高频函数,就足以写出稳定、可维护的构建脚本。真正需要诊断时,make -n 和 make -d 比反复猜快得多。