Makefile基础

make 解决的是“按依赖关系增量执行命令”的问题。
写 Makefile 的价值,不只是编译 C/C++,更是把项目里的重复构建动作标准化。

1. Makefile 到底在做什么

一句话:声明目标、依赖和命令,让 make 决定何时执行。

基本规则:

1
2
target: prerequisites
commands

关键点:

  1. commands 前必须是 Tab,不是空格。
  2. 当依赖比目标“更新”或目标不存在时,命令会执行。
  3. make 默认执行文件中的第一个目标。

2. 一个最小示例

1
2
3
4
5
6
7
8
9
10
11
12
app: main.o util.o
gcc -o app main.o util.o

main.o: main.c util.h
gcc -c main.c -o main.o

util.o: util.c util.h
gcc -c util.c -o util.o

.PHONY: clean
clean:
rm -f app *.o

执行:

1
2
make       # 构建 app
make clean # 清理产物

3. 变量:避免重复

1
2
3
4
5
6
7
CC      := gcc
CFLAGS := -O2 -Wall
TARGET := app
OBJS := main.o util.o

$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^

常见变量写法:

  1. = 递归展开(延迟求值)。
  2. := 立即展开(推荐大部分场景用它,行为更可控)。

4. 自动变量:Makefile 的高频语法

在规则命令里最常用 3 个:

  1. $@:当前目标名。
  2. $<:第一个依赖(常用于单源文件编译)。
  3. $^:全部依赖(去重后)。

示例:

1
2
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

5. 模式规则:批量处理同类文件

1
2
3
4
5
6
7
8
SRCS := main.c util.c log.c
OBJS := $(SRCS:.c=.o)

app: $(OBJS)
$(CC) -o $@ $^

%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

这种写法可扩展性强,新增源文件通常只改 SRCS

6. 伪目标:不是文件的“动作”

cleanruntest 通常不是实际文件,应该声明成伪目标:

1
.PHONY: clean run test

否则目录里若碰巧出现同名文件,目标可能不会执行。

7. 自动生成头文件依赖(实战很重要)

如果只写 %.o: %.c,头文件变化可能不会触发重编译。
推荐让编译器自动生成依赖文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CC      := gcc
CFLAGS := -O2 -Wall -MMD -MP
SRCS := main.c util.c
OBJS := $(SRCS:.c=.o)
DEPS := $(OBJS:.o=.d)

app: $(OBJS)
$(CC) $(OBJS) -o $@

%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

-include $(DEPS)

.PHONY: clean
clean:
rm -f app $(OBJS) $(DEPS)

这是我最建议保留的基础配置之一。

8. 一个更像项目的模板

目录示例:

1
2
3
4
5
6
7
project/
src/
main.c
util.c
include/
util.h
Makefile

对应 Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CC      := gcc
CFLAGS := -O2 -Wall -Iinclude -MMD -MP
SRC_DIR := src
SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(SRCS:.c=.o)
DEPS := $(OBJS:.o=.d)
TARGET := app

$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $@

%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

-include $(DEPS)

.PHONY: clean run
run: $(TARGET)
./$(TARGET)

clean:
rm -f $(TARGET) $(OBJS) $(DEPS)

9. 常见坑

  1. 命令前用了空格而不是 Tab。
  2. 没有 .PHONY,导致 clean 不执行。
  3. 忘了头文件依赖,改了 .h 却不重编译。
  4. 在同一个规则里混用太多 shell 逻辑,维护困难。

10. 我的看法

Makefile 的门槛不在语法,而在“依赖建模”:

  1. 把目标拆清楚(编译、链接、测试、打包)。
  2. 让每个目标只做一件事。
  3. 先可读,再追求花哨技巧。

基础写稳后,你会发现 Makefile 不只是“老工具”,而是工程自动化里非常耐用的一层。