7、多目录Makefile:递归make、单Makefile管理多目录、VPATH与vpath
项目一大了,源文件不可能全塞在一个目录里。你想想看,驱动放一块、算法放一块、应用层再放一块,这才像个正经工程的样子。但目录一多,Makefile怎么写就成了个问题。
我个人习惯把这个问题分成三种解法:递归make、单Makefile统一管理、以及借助VPATH/vpath来“欺骗”编译器。今天咱们就把这三种方案掰开揉碎了讲清楚。
核心问题:多目录下,make命令如何找到源文件?编译出的.o文件放哪?依赖关系怎么维护?
7.1 递归make:各扫门前雪
递归make的思路很直接——每个子目录自己写一个Makefile,然后在顶层Makefile里用make -C挨个调用它们。
我在项目中遇到过一个大坑:递归make虽然逻辑清晰,但依赖追踪很差。顶层make不知道子目录里哪些文件变了,只能傻傻地全部重编。后来我学乖了,只在子目录完全独立、互相没有头文件依赖时才用递归。
来看一个典型结构:
project/
├── Makefile # 顶层
├── main.c
├── lib/
│ ├── Makefile
│ ├── foo.c
│ └── foo.h
└── test/
├── Makefile
└── bar.c
顶层Makefile这样写:
all:
make -C lib
make -C test
gcc -o main main.c lib/foo.o test/bar.o
clean:
make -C lib clean
make -C test clean
rm -f main
注意:递归make的每个子进程都是独立的make实例,全局变量、宏定义不会传递。如果你想让子目录共享编译选项,要么用export,要么用include。我曾经因为忘记export CFLAGS,导致子目录用了不同的优化级别,查了一下午的bug。
7.2 单Makefile管理多目录:一统江湖
另一种思路是——只写一个Makefile,把所有目录的源文件都列出来。这种做法我更喜欢,因为依赖关系一目了然,增量编译也靠谱。
怎么做?说白了就是利用路径前缀:
SRC_DIRS := . lib test
SRCS := $(wildcard $(addsuffix /*.c, $(SRC_DIRS)))
OBJS := $(SRCS:.c=.o)
main: $(OBJS)
gcc -o $@ $^
%.o: %.c
gcc -c -o $@ $<
clean:
rm -f $(OBJS) main
这里有个小技巧:wildcard配合addsuffix可以一次性搜刮多个目录下的.c文件。但要注意,.o文件和.c文件会在同一个目录下生成,如果你不想污染源码目录,就得用下面讲的VPATH。
我的经验:单Makefile适合中小型项目(几十个文件)。一旦源文件超过100个,Makefile会变得臃肿,这时候我才会考虑递归make或者cmake。
7.3 VPATH与vpath:让make学会“找文件”
VPATH是make内置的一个特殊变量。你告诉它源文件可能在哪些目录,它就会自动去那些目录里找。说白了就是给make开个“寻路挂”。
用法很简单:
VPATH = lib test
main: main.o foo.o bar.o
gcc -o $@ $^
%.o: %.c
gcc -c -o $@ $<
这样,make会自动去lib/下找foo.c,去test/下找bar.c。但有个坑——VPATH只影响依赖查找,不影响目标文件生成路径。上面的写法会把.o文件生成在顶层目录,和.c文件分开了。
如果你想让.o文件也按目录存放,就得用vpath(小写)指令,它更精细:
vpath %.c lib test
lib/%.o: lib/%.c
gcc -c -o $@ $<
test/%.o: test/%.c
gcc -c -o $@ $<
这样每个.o文件都会乖乖待在对应的子目录里。
避坑指南:我曾经在项目里混用VPATH和vpath,结果make报“no rule to make target”。后来才搞明白——VPATH是全局的,vpath可以按文件类型指定。如果你两种都用,vpath的优先级更高。建议新手先只用一种,别混着来。
7.4 三种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 递归make | 模块独立、易于分工 | 依赖追踪弱、编译慢 | 大型项目、子目录完全解耦 |
| 单Makefile | 依赖完整、增量编译好 | 文件多时臃肿 | 中小型项目(<100文件) |
| VPATH/vpath | 灵活、源码与构建分离 | 规则复杂、容易出错 | 需要分离源码和构建目录 |
7.5 实战建议:我一般怎么选?
嗯,这里说说我的真实做法。如果是个人项目或者小团队(三五个人),我首选单Makefile + VPATH。因为简单、可靠,出了问题一眼就能看出来。
如果是大项目,比如有几十个模块、几百个文件,我会用递归make + 顶层依赖生成。具体做法是:每个子目录的Makefile除了编译,还会生成一个.d依赖文件,顶层make通过include把这些依赖文件汇总起来。这样既保留了模块化,又解决了依赖追踪的问题。
至于vpath,我一般只在需要把构建产物放到单独目录(比如build/)时才用。比如:
BUILD_DIR := build
vpath %.c src lib test
$(BUILD_DIR)/%.o: %.c
mkdir -p $(dir $@)
gcc -c -o $@ $<
这样所有.o文件都生成在build/下,源码目录干干净净。你想想看,提交代码时一个.gitignore把build/一屏蔽,多清爽。
最后提醒一句:不管用哪种方案,一定要在Makefile开头加上.PHONY声明伪目标。我见过太多人因为忘了这个,导致目录下有个叫clean的文件,然后make clean永远不执行——那场面,尴尬得很。