4. Makefile进阶
说实话,很多工程师写Makefile的水平,就停留在「能编译就行」。我以前也这样,觉得反正能跑,管它丑不丑。直到有一次接手一个大型项目,光源文件就两百多个,目录还分了七八层……那个Makefile写得,我自己都不想看第二遍。
后来我花了整整两天,把整个构建系统重写了。用上了自动变量、模式规则、还有那些看着像天书其实很实用的函数。嗯,从那以后,我写Makefile的习惯彻底变了。
这一章,我就把这些实战中真正好用的技巧,掰开揉碎了讲给你听。
4.1 自动变量:$@、$^、$< 到底怎么用?
先问个问题:你写编译规则的时候,是不是还在这样写?
main.o: main.c
gcc -c main.c -o main.o
utils.o: utils.c
gcc -c utils.c -o utils.o
每个目标文件都要写一遍?累不累?
自动变量就是来解决这个问题的。它们就像占位符,make会自动帮你填上正确的值。
| 自动变量 | 含义 | 举个例子 |
|---|---|---|
| $@ | 目标文件名 | 如果目标是 main.o,$@ 就是 main.o |
| $^ | 所有依赖文件(去重) | 依赖是 main.c utils.h,$^ 就是 main.c utils.h |
| $< | 第一个依赖文件 | 依赖是 main.c utils.h,$< 就是 main.c |
| $? | 比目标新的依赖文件 | 增量编译时特别有用 |
用自动变量改写上面的例子:
main.o: main.c
gcc -c $< -o $@
utils.o: utils.c
gcc -c $< -o $@
你看,规则体完全一样了。这为下一步的「模式规则」铺好了路。
4.2 模式规则:一条规则搞定所有 .c 文件
刚才虽然用了自动变量,但每个 .o 文件还是要写一条规则。能不能只写一条,让所有 .c 文件都适用?
能。模式规则就是干这个的。
%.o: %.c
gcc -c $< -o $@
这条规则的意思是:任何 .o 文件,都由同名的 .c 文件编译而来。
% 是通配符,匹配文件名中相同的部分。比如 main.o 和 main.c,% 匹配的就是 main。
加上头文件依赖呢?
%.o: %.c utils.h
gcc -c $< -o $@
这样只要 utils.h 变了,所有 .o 都会重新编译。但说实话,这种写法太死板。我一般会用自动生成依赖的方式,后面会讲到。
4.3 函数使用:wildcard 和 patsubst
写Makefile最烦什么?手动列出所有源文件。尤其是项目大了以后,加一个文件就要改一次Makefile,烦不烦?
wildcard 函数就是来救你的。
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))
第一行:找到当前目录下所有 .c 文件,赋值给 SRC。
第二行:把 SRC 里所有 .c 后缀换成 .o,赋值给 OBJ。
就这么两行,以后加文件?只管加,Makefile 不用动。
patsubst 的完整写法是:
$(patsubst 原模式, 目标模式, 文本列表)
它还有个简写形式:
OBJ = $(SRC:.c=.o)
效果完全一样,写起来更短。我个人更喜欢这种写法。
SRC = $(wildcard src/*.c)
OBJ = $(SRC:.c=.o)
TARGET = myapp
$(TARGET): $(OBJ)
gcc $^ -o $@
%.o: %.c
gcc -c $< -o $@
这段代码,基本能应付大部分中小型项目了。
4.4 多目录管理:别让源文件挤在一个文件夹里
项目大了,源文件肯定要分目录放。比如:
project/
├── src/
│ ├── main.c
│ ├── utils.c
│ └── module/
│ └── core.c
├── include/
│ └── utils.h
└── Makefile
这时候,wildcard 要加上路径:
SRC = $(wildcard src/*.c src/**/*.c)
注意:Make 的 wildcard 默认不支持 ** 递归匹配。你得用 shell 命令来帮忙:
SRC = $(shell find src -name "*.c")
这样就能找到 src 下所有子目录的 .c 文件了。
那 .o 文件放哪?我建议单独建个 build 目录,别和源文件混在一起。
BUILD_DIR = build
SRC = $(shell find src -name "*.c")
OBJ = $(patsubst src/%.c, $(BUILD_DIR)/%.o, $(SRC))
$(BUILD_DIR)/%.o: src/%.c
@mkdir -p $(dir $@)
gcc -c $< -o $@ -I include
这里有个关键点:$(dir $@) 会提取目标文件的目录部分,比如 build/module/,然后 mkdir -p 确保目录存在。这样编译出来的 .o 文件,目录结构和 src 下完全一致。
4.5 综合示例:一个完整的进阶 Makefile
说了这么多,来个完整的例子。这是我实际项目中简化出来的模板:
CC = gcc
CFLAGS = -Wall -O2 -I include
LDFLAGS = -lm
SRC_DIR = src
BUILD_DIR = build
TARGET = myapp
SRC = $(shell find $(SRC_DIR) -name "*.c")
OBJ = $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRC))
DEP = $(OBJ:.o=.d)
$(TARGET): $(OBJ)
$(CC) $^ -o $@ $(LDFLAGS)
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEP)
.PHONY: clean
clean:
rm -rf $(BUILD_DIR) $(TARGET)
这里多了个 -MMD -MP 选项,它会自动生成 .d 依赖文件,里面记录了每个 .o 文件依赖的头文件。这样头文件变了,对应的 .o 会自动重新编译。比手动写头文件依赖靠谱多了。
4.6 本章知识体系
下面这张图,是我梳理的 Makefile 进阶知识脉络。你看一眼,心里就有数了。
这张图把四个核心知识点串起来了。自动变量是基础,模式规则是骨架,函数是工具,多目录管理是实战场景。把它们组合好,你的 Makefile 就能从「能用」变成「好用」。
嗯,这一章的内容就到这里。Makefile 这东西,光看没用,你得动手写。找个项目,把里面的 Makefile 按这章的方法重构一遍,你会回来感谢我的。